import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ThreadLocalRandom;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoField.YEAR;

public class BenchDateFormat {
	private static final int COUNT = 1_000_000;
	private static final long base = ThreadLocalRandom.current().nextLong(123456789);
	private static final boolean DEBUG = false;
	private static final String[] verifies = DEBUG ? new String[COUNT] : null;

	private static void benchWebSimple() {
		var format = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss z", Locale.ENGLISH);
		format.setTimeZone(TimeZone.getTimeZone("GMT"));
		var date = new Date(0);
		var h = 0L;
		var t = System.nanoTime();
		for (int i = 0; i < COUNT; i++) {
			date.setTime(base + i * 123456789L);
			var s = format.format(date);
			if (DEBUG)
				verifies[i] = s;
			for (int j = 0, n = s.length(); j < n; j++)
				h = Long.rotateLeft(h, 13) ^ s.charAt(j);
		}
		System.out.println("benchWebSimple: " + h + ", " + (System.nanoTime() - t) / 1_000_000 + "ms");
	}

	private static void benchWebNew() {
		var zoneId = ZoneId.of("GMT");
		var h = 0L;
		var t = System.nanoTime();
		for (int i = 0; i < COUNT; i++) {
			var ms = base + i * 123456789L;
			var s = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.of(
					LocalDateTime.ofEpochSecond(ms / 1000, (int)(ms % 1000) * 1_000_000, ZoneOffset.UTC), zoneId));
			if (DEBUG && !s.equals(verifies[i])) {
				System.err.println("ERROR(" + ms + "): " + verifies[i] + " != " + s);
				break;
			}
			for (int j = 0, n = s.length(); j < n; j++)
				h = Long.rotateLeft(h, 13) ^ s.charAt(j);
		}
		System.out.println("   benchWebNew: " + h + ", " + (System.nanoTime() - t) / 1_000_000 + "ms");
	}

	private static void benchSimple() {
		TimeZone.setDefault(TimeZone.getTimeZone("+08:00")); // 避免夏令时影响
		var format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ENGLISH);
		var date = new Date(0);
		var h = 0L;
		var t = System.nanoTime();
		for (int i = 0; i < COUNT; i++) {
			date.setTime(base + i * 123456789L);
			var s = format.format(date);
			if (DEBUG)
				verifies[i] = s;
			for (int j = 0, n = s.length(); j < n; j++)
				h = Long.rotateLeft(h, 13) ^ s.charAt(j);
		}
		System.out.println("   benchSimple: " + h + ", " + (System.nanoTime() - t) / 1_000_000 + "ms");
	}

	private static void benchNew() {
		var zoneOff = ZoneOffset.ofTotalSeconds(TimeZone.getDefault().getRawOffset() / 1000);
		var dateFormatter = new DateTimeFormatterBuilder()
				.appendValue(YEAR, 4, 8, SignStyle.EXCEEDS_PAD)
				.appendLiteral('-')
				.appendValue(MONTH_OF_YEAR, 2)
				.appendLiteral('-')
				.appendValue(DAY_OF_MONTH, 2)
				.appendLiteral(' ')
				.appendValue(HOUR_OF_DAY, 2)
				.appendLiteral(':')
				.appendValue(MINUTE_OF_HOUR, 2)
				.appendLiteral(':')
				.appendValue(SECOND_OF_MINUTE, 2)
				.appendLiteral('.')
				.appendValue(MILLI_OF_SECOND, 3)
				.toFormatter(Locale.ENGLISH);
		var h = 0L;
		var t = System.nanoTime();
		for (int i = 0; i < COUNT; i++) {
			var ms = base + i * 123456789L;
			var s = dateFormatter.format(LocalDateTime.ofEpochSecond(ms / 1000, (int)(ms % 1000) * 1_000_000, zoneOff));
			if (DEBUG && !s.equals(verifies[i])) {
				System.err.println("ERROR(" + ms + "): " + verifies[i] + " != " + s);
				break;
			}
			for (int j = 0, n = s.length(); j < n; j++)
				h = Long.rotateLeft(h, 13) ^ s.charAt(j);
		}
		System.out.println("      benchNew: " + h + ", " + (System.nanoTime() - t) / 1_000_000 + "ms");
	}

	private static void benchOpt() {
		var zoneOff = TimeZone.getDefault().getRawOffset();
		var h = 0L;
		var bs = new byte[23];
		var t = System.nanoTime();
		for (int i = 0; i < COUNT; i++) {
			var ms = base + i * 123456789L + zoneOff;
			formatMs(ms, bs);
			var s = new String(bs, StandardCharsets.ISO_8859_1);
			if (DEBUG && !s.equals(verifies[i])) {
				System.err.println("ERROR(" + ms + "): " + verifies[i] + " != " + s);
				break;
			}
			for (int j = 0, n = s.length(); j < n; j++)
				h = Long.rotateLeft(h, 13) ^ s.charAt(j);
		}
		System.out.println("      benchOpt: " + h + ", " + (System.nanoTime() - t) / 1_000_000 + "ms");
	}

	//ref: https://github.com/torvalds/linux/blob/1d1bb12a8b1805ddeef9793ebeb920179fb0fa38/drivers/rtc/lib.c#L52
	public static void formatMs(final long time, final byte[] bs) {
		final int days = (int)(time / 86400_000);
		int u32tmp = (days + 719468) * 4 + 3;
		final long u64tmp = ((u32tmp % 146097) | 3) * 2939745L;
		final int dayInYear = (int)((u64tmp & 0xffff_ffffL) / 2939745) >>> 2;
		final int janOrFeb = (305 - dayInYear) >> 31;
		final int year = u32tmp / 146097 * 100 + (int)(u64tmp >>> 32) - janOrFeb;
		u32tmp = dayInYear * 2141 + 132377;
		final int mon = (u32tmp >>> 16) - (12 & janOrFeb) + 1;
		final int day = (u32tmp & 0xffff) / 2141 + 1;

		int ms = (int)(time - days * 86400_000L);
		final int hour = ms / 3600_000;
		ms -= hour * 3600_000;
		final int min = ms / 60_000;
		ms -= min * 60_000;
		final int sec = ms / 1000;
		ms -= sec * 1000;

		bs[0] = (byte)(year / 1000 + 48);
		bs[1] = (byte)(year % 1000 / 100 + 48);
		bs[2] = (byte)(year % 100 / 10 + 48);
		bs[3] = (byte)(year % 10 + 48);
		bs[4] = '-';
		bs[5] = (byte)(mon / 10 + 48);
		bs[6] = (byte)(mon % 10 + 48);
		bs[7] = '-';
		bs[8] = (byte)(day / 10 + 48);
		bs[9] = (byte)(day % 10 + 48);
		bs[10] = ' ';
		bs[11] = (byte)(hour / 10 + 48);
		bs[12] = (byte)(hour % 10 + 48);
		bs[13] = ':';
		bs[14] = (byte)(min / 10 + 48);
		bs[15] = (byte)(min % 10 + 48);
		bs[16] = ':';
		bs[17] = (byte)(sec / 10 + 48);
		bs[18] = (byte)(sec % 10 + 48);
		bs[19] = '.';
		bs[20] = (byte)(ms / 100 + 48);
		bs[21] = (byte)(ms % 100 / 10 + 48);
		bs[22] = (byte)(ms % 10 + 48);
	}

	public static void main(String[] args) {
//		var bs = new byte[23];
//		formatMs(System.currentTimeMillis(), bs);
//		System.out.println(new String(bs, StandardCharsets.ISO_8859_1));
		for (int i = 0; i < 5; i++) {
			benchWebSimple(); // 972ms
			benchWebNew(); // 279ms
			benchSimple(); // 994ms
			benchNew(); // 223ms
			benchOpt(); // 44ms
			System.out.println("------");
		}
	}
}
